winbrew_windows\deployment\msix/
install.rs1use anyhow::{Context, Result};
2use std::fs;
3use std::path::Path;
4
5use super::installed_package_full_name;
6
7use windows::Management::Deployment::{AddPackageOptions, PackageManager};
8use windows::core::HSTRING;
9
10pub fn install(download_path: &Path, package_name: &str) -> Result<String> {
17 let package_manager = PackageManager::new().context("failed to create package manager")?;
18 let package_uri = file_uri_for_path(download_path)?;
19 let options = AddPackageOptions::new().context("failed to create add package options")?;
20
21 package_manager
22 .AddPackageByUriAsync(&package_uri, &options)
23 .context("failed to start msix installation")?
24 .join()
25 .context("msix install failed")?;
26
27 installed_package_full_name(package_name)
28}
29
30fn file_uri_for_path(path: &Path) -> Result<windows::Foundation::Uri> {
31 let absolute_path =
32 fs::canonicalize(path).with_context(|| format!("failed to resolve {}", path.display()))?;
33 let file_uri = file_uri_string(&absolute_path);
34 let file_uri = HSTRING::from(file_uri);
35
36 windows::Foundation::Uri::CreateUri(&file_uri)
37 .context("failed to create file URI for msix installer")
38}
39
40fn file_uri_string(path: &Path) -> String {
41 let path = path.to_string_lossy();
42 let (scheme, path) = if let Some(path) = path.strip_prefix(r"\\?\UNC\") {
43 ("file://", path)
44 } else if let Some(path) = path.strip_prefix(r"\\?\") {
45 ("file:///", path)
46 } else if let Some(path) = path.strip_prefix(r"\\") {
47 ("file://", path)
48 } else {
49 ("file:///", path.as_ref())
50 };
51
52 let mut file_uri = String::with_capacity(scheme.len() + path.len() + path.len() / 4);
53 file_uri.push_str(scheme);
54 encode_file_uri_path_into(path, &mut file_uri);
55
56 file_uri
57}
58
59#[cfg(test)]
60fn encode_file_uri_path(path: &str) -> String {
61 let mut encoded = String::with_capacity(path.len() + path.len() / 4);
62 encode_file_uri_path_into(path, &mut encoded);
63
64 encoded
65}
66
67fn encode_file_uri_path_into(path: &str, encoded: &mut String) {
68 const HEX: &[u8; 16] = b"0123456789ABCDEF";
69
70 for ch in path.chars() {
71 if ch == '\\' {
72 encoded.push('/');
73 } else if is_uri_path_char(ch) {
74 encoded.push(ch);
75 } else {
76 let mut buffer = [0u8; 4];
77 for &byte in ch.encode_utf8(&mut buffer).as_bytes() {
78 encoded.push('%');
79 encoded.push(HEX[(byte >> 4) as usize] as char);
80 encoded.push(HEX[(byte & 0x0F) as usize] as char);
81 }
82 }
83 }
84}
85
86fn is_uri_path_char(ch: char) -> bool {
87 ch.is_ascii_alphanumeric() || matches!(ch, '/' | '-' | '.' | '_' | '~' | ':')
88}
89
90#[cfg(test)]
91mod tests {
92 use super::encode_file_uri_path;
93 use super::file_uri_string;
94 use std::path::Path;
95
96 #[test]
97 fn encode_file_uri_path_escapes_special_characters() {
98 let encoded = encode_file_uri_path(r"C:\pkg\o'ne tool\app#.msix");
99
100 assert_eq!(encoded, "C:/pkg/o%27ne%20tool/app%23.msix");
101 }
102
103 #[test]
104 fn encode_file_uri_path_keeps_safe_segments() {
105 let encoded = encode_file_uri_path(r"C:\Packages\Contoso.App\tool-1.0.msix");
106
107 assert_eq!(encoded, "C:/Packages/Contoso.App/tool-1.0.msix");
108 }
109
110 #[test]
111 fn file_uri_string_strips_verbatim_path_prefix() {
112 let uri = file_uri_string(Path::new(r"\\?\C:\pkg\o'ne tool\app#.msix"));
113
114 assert_eq!(uri, "file:///C:/pkg/o%27ne%20tool/app%23.msix");
115 }
116
117 #[test]
118 fn file_uri_string_handles_unc_paths() {
119 let uri = file_uri_string(Path::new(r"\\server\share\pkg\tool msix.appx"));
120
121 assert_eq!(uri, "file://server/share/pkg/tool%20msix.appx");
122 }
123}